/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.net;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.SyslogQuietWriter;
import org.apache.log4j.helpers.SyslogWriter;
import org.apache.log4j.spi.LoggingEvent;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.net.InetAddress;
import java.net.UnknownHostException;

// Contributors: Yves Bossel <ybossel@opengets.cl>
//               Christopher Taylor <cstaylor@pacbell.net>

/**
 * Use SyslogAppender to send log messages to a remote syslog daemon.
 * 
 * @author Ceki G&uuml;lc&uuml;
 * @author Anders Kristensen
 */
public class SyslogAppender extends AppenderSkeleton {
	// The following constants are extracted from a syslog.h file
	// copyrighted by the Regents of the University of California
	// I hope nobody at Berkley gets offended.

	/** Kernel messages */
	final static public int LOG_KERN = 0;
	/** Random user-level messages */
	final static public int LOG_USER = 1 << 3;
	/** Mail system */
	final static public int LOG_MAIL = 2 << 3;
	/** System daemons */
	final static public int LOG_DAEMON = 3 << 3;
	/** security/authorization messages */
	final static public int LOG_AUTH = 4 << 3;
	/** messages generated internally by syslogd */
	final static public int LOG_SYSLOG = 5 << 3;

	/** line printer subsystem */
	final static public int LOG_LPR = 6 << 3;
	/** network news subsystem */
	final static public int LOG_NEWS = 7 << 3;
	/** UUCP subsystem */
	final static public int LOG_UUCP = 8 << 3;
	/** clock daemon */
	final static public int LOG_CRON = 9 << 3;
	/** security/authorization messages (private) */
	final static public int LOG_AUTHPRIV = 10 << 3;
	/** ftp daemon */
	final static public int LOG_FTP = 11 << 3;

	// other codes through 15 reserved for system use
	/** reserved for local use */
	final static public int LOG_LOCAL0 = 16 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL1 = 17 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL2 = 18 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL3 = 19 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL4 = 20 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL5 = 21 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL6 = 22 << 3;
	/** reserved for local use */
	final static public int LOG_LOCAL7 = 23 << 3;

	protected static final int SYSLOG_HOST_OI = 0;
	protected static final int FACILITY_OI = 1;

	static final String TAB = "    ";

	// Have LOG_USER as default
	int syslogFacility = LOG_USER;
	String facilityStr;
	boolean facilityPrinting = false;

	// SyslogTracerPrintWriter stp;
	SyslogQuietWriter sqw;
	String syslogHost;

	/**
	 * If true, the appender will generate the HEADER (timestamp and host name)
	 * part of the syslog packet.
	 * 
	 * @since 1.2.15
	 */
	private boolean header = false;
	/**
	 * Date format used if header = true.
	 * 
	 * @since 1.2.15
	 */
	private final SimpleDateFormat dateFormat = new SimpleDateFormat(
			"MMM dd HH:mm:ss ", Locale.ENGLISH);
	/**
	 * Host name used to identify messages from this appender.
	 * 
	 * @since 1.2.15
	 */
	private String localHostname;

	/**
	 * Set to true after the header of the layout has been sent or if it has
	 * none.
	 */
	private boolean layoutHeaderChecked = false;

	public SyslogAppender() {
		this.initSyslogFacilityStr();
	}

	public SyslogAppender(Layout layout, int syslogFacility) {
		this.layout = layout;
		this.syslogFacility = syslogFacility;
		this.initSyslogFacilityStr();
	}

	public SyslogAppender(Layout layout, String syslogHost, int syslogFacility) {
		this(layout, syslogFacility);
		setSyslogHost(syslogHost);
	}

	/**
	 * Release any resources held by this SyslogAppender.
	 * 
	 * @since 0.8.4
	 */
	synchronized public void close() {
		closed = true;
		if (sqw != null) {
			try {
				if (layoutHeaderChecked && layout != null
						&& layout.getFooter() != null) {
					sendLayoutMessage(layout.getFooter());
				}
				sqw.close();
				sqw = null;
			} catch (java.io.IOException ex) {
				sqw = null;
			}
		}
	}

	private void initSyslogFacilityStr() {
		facilityStr = getFacilityString(this.syslogFacility);

		if (facilityStr == null) {
			System.err
					.println("\""
							+ syslogFacility
							+ "\" is an unknown syslog facility. Defaulting to \"USER\".");
			this.syslogFacility = LOG_USER;
			facilityStr = "user:";
		} else {
			facilityStr += ":";
		}
	}

	/**
	 * Returns the specified syslog facility as a lower-case String, e.g.
	 * "kern", "user", etc.
	 */
	public static String getFacilityString(int syslogFacility) {
		switch (syslogFacility) {
			case LOG_KERN :
				return "kern";
			case LOG_USER :
				return "user";
			case LOG_MAIL :
				return "mail";
			case LOG_DAEMON :
				return "daemon";
			case LOG_AUTH :
				return "auth";
			case LOG_SYSLOG :
				return "syslog";
			case LOG_LPR :
				return "lpr";
			case LOG_NEWS :
				return "news";
			case LOG_UUCP :
				return "uucp";
			case LOG_CRON :
				return "cron";
			case LOG_AUTHPRIV :
				return "authpriv";
			case LOG_FTP :
				return "ftp";
			case LOG_LOCAL0 :
				return "local0";
			case LOG_LOCAL1 :
				return "local1";
			case LOG_LOCAL2 :
				return "local2";
			case LOG_LOCAL3 :
				return "local3";
			case LOG_LOCAL4 :
				return "local4";
			case LOG_LOCAL5 :
				return "local5";
			case LOG_LOCAL6 :
				return "local6";
			case LOG_LOCAL7 :
				return "local7";
			default :
				return null;
		}
	}

	/**
	 * Returns the integer value corresponding to the named syslog facility, or
	 * -1 if it couldn't be recognized.
	 * 
	 * @param facilityName
	 *            one of the strings KERN, USER, MAIL, DAEMON, AUTH, SYSLOG,
	 *            LPR, NEWS, UUCP, CRON, AUTHPRIV, FTP, LOCAL0, LOCAL1, LOCAL2,
	 *            LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7. The matching is
	 *            case-insensitive.
	 * @since 1.1
	 */
	public static int getFacility(String facilityName) {
		if (facilityName != null) {
			facilityName = facilityName.trim();
		}
		if ("KERN".equalsIgnoreCase(facilityName)) {
			return LOG_KERN;
		} else if ("USER".equalsIgnoreCase(facilityName)) {
			return LOG_USER;
		} else if ("MAIL".equalsIgnoreCase(facilityName)) {
			return LOG_MAIL;
		} else if ("DAEMON".equalsIgnoreCase(facilityName)) {
			return LOG_DAEMON;
		} else if ("AUTH".equalsIgnoreCase(facilityName)) {
			return LOG_AUTH;
		} else if ("SYSLOG".equalsIgnoreCase(facilityName)) {
			return LOG_SYSLOG;
		} else if ("LPR".equalsIgnoreCase(facilityName)) {
			return LOG_LPR;
		} else if ("NEWS".equalsIgnoreCase(facilityName)) {
			return LOG_NEWS;
		} else if ("UUCP".equalsIgnoreCase(facilityName)) {
			return LOG_UUCP;
		} else if ("CRON".equalsIgnoreCase(facilityName)) {
			return LOG_CRON;
		} else if ("AUTHPRIV".equalsIgnoreCase(facilityName)) {
			return LOG_AUTHPRIV;
		} else if ("FTP".equalsIgnoreCase(facilityName)) {
			return LOG_FTP;
		} else if ("LOCAL0".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL0;
		} else if ("LOCAL1".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL1;
		} else if ("LOCAL2".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL2;
		} else if ("LOCAL3".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL3;
		} else if ("LOCAL4".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL4;
		} else if ("LOCAL5".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL5;
		} else if ("LOCAL6".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL6;
		} else if ("LOCAL7".equalsIgnoreCase(facilityName)) {
			return LOG_LOCAL7;
		} else {
			return -1;
		}
	}

	private void splitPacket(final String header, final String packet) {
		int byteCount = packet.getBytes().length;
		//
		// if packet is less than RFC 3164 limit
		// of 1024 bytes, then write it
		// (must allow for up 5to 5 characters in the PRI section
		// added by SyslogQuietWriter)
		if (byteCount <= 1019) {
			sqw.write(packet);
		} else {
			int split = header.length() + (packet.length() - header.length())
					/ 2;
			splitPacket(header, packet.substring(0, split) + "...");
			splitPacket(header, header + "..." + packet.substring(split));
		}
	}

	public void append(LoggingEvent event) {

		if (!isAsSevereAsThreshold(event.getLevel()))
			return;

		// We must not attempt to append if sqw is null.
		if (sqw == null) {
			errorHandler
					.error("No syslog host is set for SyslogAppedender named \""
							+ this.name + "\".");
			return;
		}

		if (!layoutHeaderChecked) {
			if (layout != null && layout.getHeader() != null) {
				sendLayoutMessage(layout.getHeader());
			}
			layoutHeaderChecked = true;
		}

		String hdr = getPacketHeader(event.timeStamp);
		String packet = layout.format(event);
		if (facilityPrinting || hdr.length() > 0) {
			StringBuffer buf = new StringBuffer(hdr);
			if (facilityPrinting) {
				buf.append(facilityStr);
			}
			buf.append(packet);
			packet = buf.toString();
		}

		sqw.setLevel(event.getLevel().getSyslogEquivalent());
		//
		// if message has a remote likelihood of exceeding 1024 bytes
		// when encoded, consider splitting message into multiple packets
		if (packet.length() > 256) {
			splitPacket(hdr, packet);
		} else {
			sqw.write(packet);
		}

		if (layout.ignoresThrowable()) {
			String[] s = event.getThrowableStrRep();
			if (s != null) {
				for (int i = 0; i < s.length; i++) {
					if (s[i].startsWith("\t")) {
						sqw.write(hdr + TAB + s[i].substring(1));
					} else {
						sqw.write(hdr + s[i]);
					}
				}
			}
		}
	}

	/**
	 * This method returns immediately as options are activated when they are
	 * set.
	 */
	public void activateOptions() {
		if (header) {
			getLocalHostname();
		}
		if (layout != null && layout.getHeader() != null) {
			sendLayoutMessage(layout.getHeader());
		}
		layoutHeaderChecked = true;
	}

	/**
	 * The SyslogAppender requires a layout. Hence, this method returns
	 * <code>true</code>.
	 * 
	 * @since 0.8.4
	 */
	public boolean requiresLayout() {
		return true;
	}

	/**
	 * The <b>SyslogHost</b> option is the name of the the syslog host where log
	 * output should go. A non-default port can be specified by appending a
	 * colon and port number to a host name, an IPv4 address or an IPv6 address
	 * enclosed in square brackets.
	 * 
	 * <b>WARNING</b> If the SyslogHost is not set, then this appender will
	 * fail.
	 */
	public void setSyslogHost(final String syslogHost) {
		this.sqw = new SyslogQuietWriter(new SyslogWriter(syslogHost),
				syslogFacility, errorHandler);
		// this.stp = new SyslogTracerPrintWriter(sqw);
		this.syslogHost = syslogHost;
	}

	/**
	 * Returns the value of the <b>SyslogHost</b> option.
	 */
	public String getSyslogHost() {
		return syslogHost;
	}

	/**
	 * Set the syslog facility. This is the <b>Facility</b> option.
	 * 
	 * <p>
	 * The <code>facilityName</code> parameter must be one of the strings KERN,
	 * USER, MAIL, DAEMON, AUTH, SYSLOG, LPR, NEWS, UUCP, CRON, AUTHPRIV, FTP,
	 * LOCAL0, LOCAL1, LOCAL2, LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7. Case is
	 * unimportant.
	 * 
	 * @since 0.8.1
	 */
	public void setFacility(String facilityName) {
		if (facilityName == null)
			return;

		syslogFacility = getFacility(facilityName);
		if (syslogFacility == -1) {
			System.err.println("[" + facilityName
					+ "] is an unknown syslog facility. Defaulting to [USER].");
			syslogFacility = LOG_USER;
		}

		this.initSyslogFacilityStr();

		// If there is already a sqw, make it use the new facility.
		if (sqw != null) {
			sqw.setSyslogFacility(this.syslogFacility);
		}
	}

	/**
	 * Returns the value of the <b>Facility</b> option.
	 */
	public String getFacility() {
		return getFacilityString(syslogFacility);
	}

	/**
	 * If the <b>FacilityPrinting</b> option is set to true, the printed message
	 * will include the facility name of the application. It is <em>false</em>
	 * by default.
	 */
	public void setFacilityPrinting(boolean on) {
		facilityPrinting = on;
	}

	/**
	 * Returns the value of the <b>FacilityPrinting</b> option.
	 */
	public boolean getFacilityPrinting() {
		return facilityPrinting;
	}

	/**
	 * If true, the appender will generate the HEADER part (that is, timestamp
	 * and host name) of the syslog packet. Default value is false for
	 * compatibility with existing behavior, however should be true unless there
	 * is a specific justification.
	 * 
	 * @since 1.2.15
	 */
	public final boolean getHeader() {
		return header;
	}

	/**
	 * Returns whether the appender produces the HEADER part (that is, timestamp
	 * and host name) of the syslog packet.
	 * 
	 * @since 1.2.15
	 */
	public final void setHeader(final boolean val) {
		header = val;
	}

	/**
	 * Get the host name used to identify this appender.
	 * 
	 * @return local host name
	 * @since 1.2.15
	 */
	private String getLocalHostname() {
		if (localHostname == null) {
			try {
				InetAddress addr = InetAddress.getLocalHost();
				localHostname = addr.getHostName();
			} catch (UnknownHostException uhe) {
				localHostname = "UNKNOWN_HOST";
			}
		}
		return localHostname;
	}

	/**
	 * Gets HEADER portion of packet.
	 * 
	 * @param timeStamp
	 *            number of milliseconds after the standard base time.
	 * @return HEADER portion of packet, will be zero-length string if header is
	 *         false.
	 * @since 1.2.15
	 */
	private String getPacketHeader(final long timeStamp) {
		if (header) {
			StringBuffer buf = new StringBuffer(dateFormat.format(new Date(
					timeStamp)));
			// RFC 3164 says leading space, not leading zero on days 1-9
			if (buf.charAt(4) == '0') {
				buf.setCharAt(4, ' ');
			}
			buf.append(getLocalHostname());
			buf.append(' ');
			return buf.toString();
		}
		return "";
	}

	/**
	 * Set header or footer of layout.
	 * 
	 * @param msg
	 *            message body, may not be null.
	 */
	private void sendLayoutMessage(final String msg) {
		if (sqw != null) {
			String packet = msg;
			String hdr = getPacketHeader(new Date().getTime());
			if (facilityPrinting || hdr.length() > 0) {
				StringBuffer buf = new StringBuffer(hdr);
				if (facilityPrinting) {
					buf.append(facilityStr);
				}
				buf.append(msg);
				packet = buf.toString();
			}
			sqw.setLevel(6);
			sqw.write(packet);
		}
	}
}
